-
Notifications
You must be signed in to change notification settings - Fork 11
simple DML translation #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
}); | ||
} | ||
|
||
public static final class MutateTranslatorAwareDialect extends Dialect { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An important feature to test is we need to include the table names involved in DML queries into the result of SqlAstTranslator#getAffectedTableNames()
which acts as auto-flush purpose in the upper level caller of the translator.
We might need to finish other features (like table joining) to test the auto-flush features in integration test; currently we could only test this important feature by some expedient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is related to auto-flushing criteria No..2 (see https://docs.jboss.org/hibernate/orm/6.6/userguide/html_single/Hibernate_User_Guide.html#flushing-auto):
- prior to executing a JPQL/HQL query that overlaps with the queued entity actions
@@ -36,17 +38,17 @@ | |||
|
|||
@SessionFactory(exportSchema = false) | |||
@ExtendWith(MongoExtension.class) | |||
abstract class AbstractSelectionQueryIntegrationTests implements SessionFactoryScopeAware, ServiceRegistryScopeAware { | |||
public abstract class AbstractQueryIntegrationTests implements SessionFactoryScopeAware, ServiceRegistryScopeAware { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this class is reusable not only for select
query but also for mutate
query, so it is renamed and moved to more general location. The Book
class was changed accordingly to go together with it hand in hand.
throw new FeatureNotSupportedException("Unsupported mutation statement type: " | ||
+ mutationStatement.getClass().getName()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently there is no fourth class of MutationStatement
; the above last else clause is for future-proof purpose.
25d8fb7
to
8e2cee5
Compare
src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommand.java
Outdated
Show resolved
Hide resolved
8e2cee5
to
55ab573
Compare
…t/command/AstInsertCommand.java Co-authored-by: Jeff Yemin <[email protected]>
checkMutationStatement(insertStatement); | ||
if (insertStatement.getConflictClause() != null) { | ||
throw new FeatureNotSupportedException(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HQL has an interesting ON CONFLICT during insertion feature (see https://www.baeldung.com/hibernate-insert-query-on-conflict-clause). It is sort of similar to MongoDB's upsert
feature but it seems there is a big difference between them:
- HQL mainly focuses on conflict during insertion (w.r.t. unique constraint or fields combination); MongoDB's
upsert
focuses on conflict during updating (w.r.t. some query or filter,, e.g. comparison between a field and a value)
So I guess we can't support this HQL feature. HQL's update statement is simpler and provides no possibility to emulate MongoDB's upsert
feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could support the same semantics in MQL.
1. "on conflict do update:
What HQL does:
insert … on conflict do update
- checks a unique index; if the key is taken it runs the UPDATE
clause.
What MongoDB does:
update … { upsert:true }
matches a filter; if no doc matches it inserts instead.
Despite starting from opposite directions (insert-first vs. update-first) they end up with the same two-way branch:
Case | HQL | MongoDB |
---|---|---|
No existing row/doc | run regular INSERT | insert via upsert |
Conflict / match found | run DO UPDATE … | run UPDATE … |
We could put the “excluded” columns in $set, and use $setOnInsert for values present only on insert.
Example command:
{
updateOne: {
filter: { _id: 1 },
update: {
// similar to DO UPDATE … SET title = excluded.title
$set: { title: "Pride & Prejudice" },
// fields present only on first insert
$setOnInsert: {
title: "Pride & Prejudice"
outOfStock: false,
publishYear: 1813,
isbn13: NumberLong("9780141439518"),
discount: 0.2,
price: Decimal128("23.55")
}
},
upsert: true
}
},
2. "on conflict do nothing":
We could use ordered: false
for insert operations to process all inserts and ignore any duplication errors reported via writeErrors
. For this, though, we’d need to be aware of the HQL context, if possible.
In any case, I suggest we handle this as a separate ticket, so as not to block this PR and to keep the review focused. Let me know your thoughts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitively it deserves a ticket. I think all of the following HQL criteria needs to be satisfied to allow for MQL translation:
- ON CONFLICT is specified
- ON CONSTRAINT uniqueIndexName is not used as the criteria (MQL's filter can't be based on unique index name, correct?)
- CONFLICT ACTION is not NOTHING
so it seems the ticket is an interesting tech one but could be used very rarely in reality; native query seems more flixible and powerful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created a ticket here: https://jira.mongodb.org/browse/HIBERNATE-94. Let me know when you have any further suggestion.
} | ||
if (insertStatement.getSourceSelectStatement() != null) { | ||
throw new FeatureNotSupportedException(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HQL has two flavours of insertion statement:
insert into Book (id, title) values (1, "War and Peace"), (2, "Crime and Punishment")
insert into Book (id, title) select c.identifier, c.name from Catalog as c
This PR will focus on the first variant and throw exception for the latter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have a ticket for the latter? If not, should we create one in the backlog to track it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have a ticket. From my understanding, there is no MQL counterpart so we won't support it in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have $out
and $merge
. Neither is a direct counterpart to insert into ... select ...
, despite $out
being advertized as the counterpart (it "conveniently" for applications deletes the target collection, which SQL never does), but maybe there is a way to implement insert into ... select ...
in our extension in a way that at least partially mimics the expected behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did consider the possibility to emulate insert into ... select ...
using aggregate pipeline, but I am not sure whether it is worthwhile; one of the reasons is our user could easily achieve the same goal by breaking down the process to two separate steps programmatically (first grabbing all the values using some selection or native query and then using the normal insertion mutation or native query) and it is much more flexible.
If you persist, I can create a ticket of lowest priority.
documents.add(new AstDocument(astElements)); | ||
} | ||
|
||
astVisitorValueHolder.yield(COLLECTION_MUTATION, new AstInsertCommand(collection, documents)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are 3 assertion usages above as follows:
- insertion field list is not empty
- insertion values list is not empty
- field list length is the same as values length
The above constraints were either enforced from HQL grammar level or semantic checking phase in Hibernate (a prior step when the final SQL AST translation step is reached), so we can rest assured these constraints are enforced already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly, various assertions are used in updating case as well. Common constraints have been carefully centralized into checkMutationStatementSupportability()
method. Only one checking is required to ensure the insertion field value or udpating field value expressions should be either valueExpression
(literal value or parameter expressions).
42d2b77
to
5c7a58d
Compare
0cc0a35
to
4d24c09
Compare
4d24c09
to
6558ab1
Compare
…he old selection queries testing caeses
# Conflicts: # src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java # src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java
src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
Outdated
Show resolved
Hide resolved
src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
Outdated
Show resolved
Hide resolved
return delegate.getSqlAstTranslatorFactory().buildModelMutationTranslator(mutation, sessionFactory); | ||
} | ||
|
||
private <T extends JdbcOperation> SqlAstTranslator<T> createCapturingTranslator( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Let’s make the name a bit more descriptive here to show the intent, something like createTranslateResultCapturingAstTranslator
.
src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java
Show resolved
Hide resolved
src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
Outdated
Show resolved
Hide resolved
src/integrationTest/java/com/mongodb/hibernate/query/mutation/InsertionIntegrationTests.java
Show resolved
Hide resolved
…tMqlTranslator.java Co-authored-by: Viacheslav Babanin <[email protected]>
…tMqlTranslator.java Co-authored-by: Viacheslav Babanin <[email protected]>
…ractMqlTranslator
…InsertionIntegrationTests.java Co-authored-by: Viacheslav Babanin <[email protected]>
…se 'is not supported' as opposed to 'not supported'
@stIncMale , would appreciate your taking a secondary review for @vbabanin had approved it for a while. Thanks. |
private static void checkCteContainerSupportability(CteContainer cteContainer) { | ||
if (!cteContainer.getCteStatements().isEmpty() | ||
|| !cteContainer.getCteObjects().isEmpty()) { | ||
throw new FeatureNotSupportedException("CTE is "); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message is incomplete.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, thanks!
} | ||
var root = fromClause.getRoots().get(0); | ||
if (root.hasRealJoins()) { | ||
throw new FeatureNotSupportedException("TODO-HIBERNATE-65 https://jira.mongodb.org/browse/HIBERNATE-65"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://jira.mongodb.org/browse/HIBERNATE-65 is linked here in the TODO
comment, but there is no note in the ticket description about resolving such TODO
s. We must add it similarly to how it is done in https://jira.mongodb.org/browse/HIBERNATE-44, for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, we should have. Done.
private String addToAffectedCollections(NamedTableReference tableRef) { | ||
var collection = tableRef.getTableExpression(); | ||
affectedTableNames.add(collection); | ||
return collection; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field containing the list is named affectedTableNames
, but the method adding to the list is named addToAffectedCollections
. Let's rename the method to addToAffectedTableNames
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My original intention is to map closer to our domain model at least on method level (field name is lower-level and mapped to Hibernate's particular); but I changed as you suggested.
} | ||
if (insertStatement.getSourceSelectStatement() != null) { | ||
throw new FeatureNotSupportedException(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have $out
and $merge
. Neither is a direct counterpart to insert into ... select ...
, despite $out
being advertized as the counterpart (it "conveniently" for applications deletes the target collection, which SQL never does), but maybe there is a way to implement insert into ... select ...
in our extension in a way that at least partially mimics the expected behavior.
@@ -60,6 +58,12 @@ private static List<Book> getBooksByIds(int... ids) { | |||
.toList(); | |||
} | |||
|
|||
@BeforeEach | |||
protected void beforeEach() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this method moved from the top of the class? We previously moved all such methods to the top specifically to make the code consistent, instead of having them scattered in different places of the class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't notice such convention and I tried to be consistent with LimitOffsetFetchClauseIntegrationTests
; relocate @BeforeEach
in both classes after field declaration.
@Override | ||
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { | ||
this.sessionFactoryScope = sessionFactoryScope; | ||
} | ||
|
||
@Override | ||
public void injectServiceRegistryScope(ServiceRegistryScope serviceRegistryScope) { | ||
this.testCommandListener = serviceRegistryScope.getRegistry().requireService(TestCommandListener.class); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move these methods to the top of the class, right after the field declarations. That way they will be placed consistently with the intentional placement in the other classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, didn't notice we have such convention.
|
||
private Book bookOutOfStock; | ||
private Book bookInStock; | ||
|
||
@BeforeEach | ||
void beforeEach() { | ||
protected void beforeEach() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for making this method protected
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a legacy minor issue; tbh I don't recollect the reason. Removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not a legacy issue; I overlooked it during refactoring; I tried to move some common beforeEach()
to the AbstractQueryIntegrationTests
parent class and then protected
might be needed; later on I aborted the experimenting and forgot to remove it.
@@ -60,6 +58,12 @@ private static List<Book> getBooksByIds(int... ids) { | |||
.toList(); | |||
} | |||
|
|||
@BeforeEach | |||
protected void beforeEach() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for making this method protected? As far as I can tell, it should be package-access.
protected void beforeEach() { | |
void beforeEach() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java
Outdated
Show resolved
Hide resolved
src/integrationTest/java/com/mongodb/hibernate/query/mutation/InsertionIntegrationTests.java
Show resolved
Hide resolved
The last reviewed commit is ea75d2f. |
…ueryIntegrationTests.java Co-authored-by: Valentin Kovalenko <[email protected]>
…UpdatingIntegrationTests.java Co-authored-by: Valentin Kovalenko <[email protected]>
https://jira.mongodb.org/browse/HIBERNATE-46
The scope of the ticket is to unblock the following simple DML HQL mutation queries:
insert into Book (id, title) values (1, "War and Peace"), (2, "Crime and Punishmnet")
update Book set title = "War and Peace" where title = "War & Peace"
delete from Book where title = "War and Peace"
Translation is straightforward. The following new visitor methods would be implemented in this PR:
Both of the first two methods share a lot in common in the sense that they are both based on where clause, with various AST filters as the backbones, which has been finished in previous PR.
The scope of this PR is modest and only focuses on the following basic stuff:
$set
updating operator is supported (with the constraint that$set
focuses on field path and value pair)One caveat is previously our insertion command mongo ast only allows one document, to fully support HQL's feature, the mongo ast model was refactored to allow multiple documents, consistent with MQL's syntax as well. There is a separate ticket for this requirment: https://jira.mongodb.org/browse/HIBERNATE-44, so we can finish it in this PR additionally.